﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using ReAnimation.MVP.Models.Interfaces;
using ReAnimation.Helpers.Common;
using GLWrap;
using GLWrap.SceneObjects;
using ReAnimation.Infrastructure.Services.Interfaces;
using GLWrap.SceneObjects.Line;
using Sharp3D.Math.Core;
using GLWrap.MathExt;
using ReAnimation.Domain;
using System.Drawing;
using System.Diagnostics;
using ReAnimation.Infrastructure.Services.DomainFactories.Interfaces;
using ReAnimation.Helpers.WinForms;
using ReAnimation.Helpers.IoC;
using System.Drawing.Imaging;

namespace ReAnimation.Infrastructure.Services
{
    public class AnimationToPOVService: IAnimationToPOVService
    {
        IApplicationSettings _applicationSettings;
        ILazyInject<INotifier> _notifierInj;
        IMainModel _mainModel;
        IGlSceneManager _glSceneManager;
        IZMatrixService _zMatrixService;
        IAnimationFactory _animationFactory;


        public AnimationToPOVService(
            IApplicationSettings applicationSetttings,
            ILazyInject<INotifier> notifierInj, IMainModel mainModel, IGlSceneManager glSceneManager,IAnimationFactory animationFactory,
            IZMatrixService zMatrixService)
        {
            _applicationSettings = applicationSetttings;
            _notifierInj = notifierInj;
            _zMatrixService = zMatrixService;
            _mainModel = mainModel;
            _glSceneManager = glSceneManager;
            _animationFactory = animationFactory;
        }

        public void ConvertAnimationToPOV(double durationInMinutes, Size resolution, bool saveFrames,bool usePov, String filePath,Action<float> updateStats)
        {
            String dir = Path.GetDirectoryName(filePath);
            DateTime begin = DateTime.Now;
            uint framesCount = (uint) (durationInMinutes * 60.0f * 25.0f);// 25 fps
            uint frameNumber = 0;
            FileSystemWatcher w = new FileSystemWatcher(dir);

            IEnumerable<AnimationStep> steps = _mainModel.CurrentAnimation.AnimationSteps.OrderBy((x)=>{return x.Percent;}).ToList();

            foreach (AnimationStep animationStep in steps)
            {
                if (steps.Last() == animationStep) break;

                AnimationStep nextAnimationStep = steps.ElementAt(
                    steps.ToList().IndexOf(animationStep)+1);

                float percentDuration = (nextAnimationStep.Percent - animationStep.Percent);
                int stepFramesCount = (int) ((float)framesCount / 100.0f * percentDuration);

                (stepFramesCount-1).TimesDo((i) =>
                    {
                        String fileName = "{0}/file_{1}.pov".F(dir, frameNumber);
                        StreamWriter sw = new StreamWriter(fileName);
                        AnimationStep currentAnimationStep = _animationFactory.CloneAnimationStep(animationStep);
                        if(i!=0)
                        {
                            float percent = (float)i / (float)(stepFramesCount-1) * 100.0f;
                            ICamera cam = InterpolateCamera(animationStep.Camera,nextAnimationStep.Camera, percent);
                            currentAnimationStep.Camera = cam;

                            foreach (Molecule m in currentAnimationStep.Molecules)
                            {
                                Molecule nextM = nextAnimationStep.Molecules.FirstOrDefault((mol) =>
                                    {
                                        return mol.Name.Equals(m.Name);
                                    });
                                InterpolateMolecule(m, nextM, percent);
                            }
                        }

                        if(usePov)
                            ConvertAnimationStepToPOV(currentAnimationStep, sw);

                        sw.Close();

                        string @params = "/RENDER /EXIT +O\"{0}/file_{1}.png\" ".F(dir, frameNumber) + 
                            "+W{0} +H{1} ".F(resolution.Width,resolution.Height)+ 
                            " {0} ".F(_applicationSettings.PovRayParams)+ " \"{0}/file_{1}.pov\"".F(dir, frameNumber);

                        if (usePov)
                        {
                            Process p = Process.Start(_applicationSettings.PovRayPath, @params);

                            p.WaitForExit();
                        }
                        else
                        {
                            AnimationStep step = _mainModel.CurrentAnimationStep;

                            _mainModel.CurrentAnimationStep = currentAnimationStep;

                            Bitmap b = _glSceneManager.GrabScreenshot();

                            b.Save("{0}/file_{1}.png".F(dir, frameNumber), ImageFormat.Png);
                            b.Dispose();
                            _mainModel.CurrentAnimationStep = step;
                        }

                        File.Delete("{0}/file_{1}.pov".F(dir,frameNumber));
                       
                        updateStats((float)frameNumber/(float)framesCount * 100.0f);

                        frameNumber++;
                    });
            }
            DateTime end = DateTime.Now;

            updateStats(100);
            INotifier not = _notifierInj.Get();
            not.ShowMessage("Finished rendering", "Finished in {0} seconds".F((end - begin).TotalSeconds));

            Process pr = Process.Start("ffmpeg.exe", "-r 25 -i \"{0}/file_%d.png\" {1} {2}".F(dir,_applicationSettings.FfmpegParams,filePath));
            pr.WaitForExit();

            if(!saveFrames)
            ((int)frameNumber).TimesDo((i) =>
                {
                    File.Delete("{0}/file_{1}.png".F(dir,i));
                });
        }

        private void InterpolateMolecule(Molecule m1, Molecule m2, float percent)
        {
            if(m2==null) return;

            Vector3F startTranslateV = new Vector3F(m1.TransformMatrix.M14, m1.TransformMatrix.M24, m1.TransformMatrix.M34);
            Vector3F endTranslateV = new Vector3F(m2.TransformMatrix.M14, m2.TransformMatrix.M24, m2.TransformMatrix.M34);

            Vector3F nV = InterpolateVector(startTranslateV, endTranslateV, percent);

            QuaternionF q1 = m1.TransformMatrix.GetQuaternion();
            QuaternionF q2 = m2.TransformMatrix.GetQuaternion();
            q1.Normalize();
            q2.Normalize();

            float inner = QuaternionF.DotProduct(q1, q2);
            if (inner < 0) q2 *= -1;
            q2 -= q1;

            QuaternionF result = q1 + q2 * (percent / 100.0f);

            Matrix4F mat = result.GetRotationMatrix();
            mat.M14 = nV.X;
            mat.M24 = nV.Y;
            mat.M34 = nV.Z;

            m1.TransformMatrix = mat;

            if (m1.MoleculeZMatrix.ZMatrixLines.Count == m2.MoleculeZMatrix.ZMatrixLines.Count)
            {
                m1.MoleculeZMatrix.ZMatrixLines.Count.TimesDo((i) =>
                    {
                        m1.MoleculeZMatrix.ZMatrixLines[i].BondAngle =
                            InterpolateFloat(m1.MoleculeZMatrix.ZMatrixLines[i].BondAngle,
                            m2.MoleculeZMatrix.ZMatrixLines[i].BondAngle, percent);

                        m1.MoleculeZMatrix.ZMatrixLines[i].BondDistance =
                            InterpolateFloat(m1.MoleculeZMatrix.ZMatrixLines[i].BondDistance,
                            m2.MoleculeZMatrix.ZMatrixLines[i].BondDistance, percent);

                        m1.MoleculeZMatrix.ZMatrixLines[i].DihedralAngle =
                            InterpolateFloat(m1.MoleculeZMatrix.ZMatrixLines[i].DihedralAngle,
                            m2.MoleculeZMatrix.ZMatrixLines[i].DihedralAngle, percent);
                    });

                _zMatrixService.ConvertZMatrixToCartesian(m1.MoleculeZMatrix, m1);
            }
        }

        private float InterpolateFloat(float v1, float v2, float percent)
        {
            return v1 + (v2 - v1) * percent / 100.0f;
        }

        private Vector3F InterpolateVector(Vector3F startVector, Vector3F endVector, float percent)
        {
            return new Vector3F(
                startVector.X + (endVector.X-startVector.X)*percent/100.0f,
                startVector.Y + (endVector.Y-startVector.Y)*percent/100.0f,
                startVector.Z + (endVector.Z-startVector.Z)*percent/100.0f
                );
        }

        private ICamera InterpolateCamera(ICamera cam1, ICamera cam2, float percent)
        {
            return new Camera()
            {
                Eye = cam1.Eye + (cam2.Eye - cam1.Eye)/100.0f*percent,
                View = cam1.View + (cam2.View - cam1.View)/100.0f*percent,
                Up = cam2.Up
            };
        }

        public void ConvertAnimationStepToPOV(AnimationStep animationStep,StreamWriter streamWriter)
        {
            AnimationStep oldAnimationStep = _mainModel.CurrentAnimationStep;
            Molecule oldCurrentMolecule = _mainModel.CurrentMolecule;

            _mainModel.CurrentAnimationStep = animationStep;
            _mainModel.CurrentMolecule = animationStep.Molecules.FirstOrDefault();

            AddUnifiedHeader(streamWriter);
            AddCamera(streamWriter);
            AddLights(streamWriter);
            AddObjects(streamWriter);

            _mainModel.CurrentAnimationStep = oldAnimationStep;
            _mainModel.CurrentMolecule = oldCurrentMolecule;
        }

        private void AddUnifiedHeader(StreamWriter streamWriter)
        {
            streamWriter.WriteLine(
@"global_settings 
{{
    ambient_light rgb <0.200000002980232, 0.200000002980232, 0.200000002980232>
    max_trace_level 15
}}
background {{ color rgb <{0},{1},{2}> }}".F(
                                         
                                         ((float)_applicationSettings.BackgroundColor.R/255.0f).ToString().Replace(",","."),
((float)_applicationSettings.BackgroundColor.G/255.0f).ToString().Replace(",","."),
((float)_applicationSettings.BackgroundColor.B/255.0f).ToString().Replace(",",".")
                                         
                                         )
            );
        }

        private void AddCamera(StreamWriter streamWriter)
        {
           ICamera camera = _glSceneManager.Camera;

           Vector3F up = camera.Up * _glSceneManager.Top;
           Vector3F right = Vector3F.CrossProduct(camera.View - camera.Eye, camera.Up);
           right.Normalize();
           right *= _glSceneManager.Right;
            Vector3F direction = new Vector3F(camera.View.X - camera.Eye.X, camera.View.Y - camera.Eye.Y, camera.View.Z - camera.Eye.Z);
            direction.Normalize();
            

           streamWriter.WriteLine(
@"camera {{perspective
location <{0}, {1}, {2}>
angle {3}
up <{4}, {5}, {6}>
right <{7},{8},{9}>
direction <{10}, {11}, {12}> 
// width = {13}, height = {14}
}}".F(camera.Eye.X,camera.Eye.Y,camera.Eye.Z,
  45,
  up.X,up.Y,up.Z,
  right.X,right.Y,right.Z,
  direction.X,direction.Y,direction.Z,
  _glSceneManager.Width,_glSceneManager.Height)
                );
        }

        private void AddLights(StreamWriter streamWriter)
        {
            streamWriter.WriteLine(
@"light_source {
	<-9.15655920349864, 6.6306909404542, 9.59488871488939>
	color rgb <1, 1, 1>
	fade_distance 20.32
	fade_power 0
	parallel
	point_at <9.15655920349864, -6.6306909404542, -9.59488871488939>
}

light_source {
	<9.00914026347842, -5.59764072355214, 5.47294603382051>
	color rgb <0.300000011920929, 0.300000011920929, 0.300000011920929>
	fade_distance 20.32
	fade_power 0
	parallel
	point_at <-9.00914026347842, 5.59764072355214, -5.47294603382051>
}

#default {
	finish {ambient .8 diffuse 1 specular 1 roughness .005 metallic 0.5}
}"
                );
        }

        private void AddObjects(StreamWriter streamWriter)
        {
            streamWriter.WriteLine("union{");
            {
                foreach(SceneObject obj in _glSceneManager.SceneObjects)
                    AddObjects(streamWriter,obj,Matrix4F.Identity);
            }
            streamWriter.WriteLine("}");
        }

        private void AddObjects(StreamWriter streamWriter, SceneObject @object, Matrix4F mat)
        {
            SimpleLine line = @object as SimpleLine;
            if (line != null)
            {
                Matrix4F m = mat;
                Vector3F p1 = m.Multiply(line.PointA);
                Vector3F p2 = m.Multiply(line.PointB);
                streamWriter.WriteLine(
@"cylinder {{
	<{0},{1},{2}>,<{3},{4},{5}>, {6}
	pigment {{ rgbt <{7}, {8}, {9}, 0> }}
}}".F(p1.X, p1.Y, p1.Z - 1,
p2.X, p2.Y, p2.Z - 1,
line.Weight,
line.Material.Diffuse.X, line.Material.Diffuse.Y, line.Material.Diffuse.Z, 1 - line.Material.Diffuse.W)
                    );
                return;
            }

            SceneObjectsGroup group = @object as SceneObjectsGroup;
            if (group != null)
            {
                foreach (SceneObject sceneObject in group.AttachedObjects)
                {
                    AddObjects(streamWriter, sceneObject, mat*group.TransformationMatrix);
                }
                return;
            }
                Sphere sphere = @object as Sphere;
                if (sphere != null)
                {
                    Matrix4F m = mat * sphere.TransformationMatrix;
                    Vector3F position = sphere.Position;
                    position = m.Multiply(position);

                    streamWriter.WriteLine(
@"sphere {{
<{0}, {1}, {2}>, {3}
pigment {{ rgbt <{4}, {5}, {6}, {7}> }}
}}".F(position.X, position.Y, position.Z - 1, sphere.Radius,
sphere.Material.Diffuse.X, sphere.Material.Diffuse.Y, sphere.Material.Diffuse.Z, 1-sphere.Material.Diffuse.W)
                        );
                    return;
                }

               
         }
    }
}
